1   // Licensed under the Apache License, Version 2.0 (the "License");
2   // you may not use this file except in compliance with the License.
3   // You may obtain a copy of the License at
4   //
5   // http://www.apache.org/licenses/LICENSE-2.0
6   //
7   // Unless required by applicable law or agreed to in writing, software
8   // distributed under the License is distributed on an "AS IS" BASIS,
9   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  // See the License for the specific language governing permissions and
11  // limitations under the License.
12  
13  package org.apache.tapestry5.internal.structure;
14  
15  import org.apache.tapestry5.*;
16  import org.apache.tapestry5.annotations.*;
17  import org.apache.tapestry5.dom.Element;
18  import org.apache.tapestry5.internal.AbstractEventContext;
19  import org.apache.tapestry5.internal.InternalComponentResources;
20  import org.apache.tapestry5.internal.InternalConstants;
21  import org.apache.tapestry5.internal.services.ComponentEventImpl;
22  import org.apache.tapestry5.internal.services.Instantiator;
23  import org.apache.tapestry5.internal.util.NamedSet;
24  import org.apache.tapestry5.internal.util.NotificationEventCallback;
25  import org.apache.tapestry5.ioc.BaseLocatable;
26  import org.apache.tapestry5.ioc.Invokable;
27  import org.apache.tapestry5.ioc.Location;
28  import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
29  import org.apache.tapestry5.ioc.internal.util.InternalUtils;
30  import org.apache.tapestry5.ioc.internal.util.Orderer;
31  import org.apache.tapestry5.ioc.internal.util.TapestryException;
32  import org.apache.tapestry5.ioc.services.PerThreadValue;
33  import org.apache.tapestry5.ioc.util.AvailableValues;
34  import org.apache.tapestry5.ioc.util.UnknownValueException;
35  import org.apache.tapestry5.model.ComponentModel;
36  import org.apache.tapestry5.model.ParameterModel;
37  import org.apache.tapestry5.runtime.Component;
38  import org.apache.tapestry5.runtime.*;
39  import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
40  import org.slf4j.Logger;
41  
42  import java.util.*;
43  
44  /**
45   * Implements {@link RenderCommand} and represents a component within an overall page. Much of a
46   * component page
47   * element's behavior is delegated to user code, via a {@link org.apache.tapestry5.runtime.Component} instance.
48   *
49   * Once instantiated, a ComponentPageElement should be registered as a
50   * {@linkplain org.apache.tapestry5.internal.structure.Page#addLifecycleListener(org.apache.tapestry5.runtime.PageLifecycleListener)
51   * lifecycle listener}. This could be done inside the constructors, but that tends to complicate unit tests, so its done
52   * by {@link org.apache.tapestry5.internal.services.PageElementFactoryImpl}. There's still a bit of refactoring in this
53   * class (and its many inner classes) that can improve overall efficiency.
54   *
55   * Modified for Tapestry 5.2 to adjust for the no-pooling approach (shared instances with externalized mutable state).
56   */
57  public class ComponentPageElementImpl extends BaseLocatable implements ComponentPageElement
58  {
59      /**
60       * Placeholder for the body used when the component has no real content.
61       */
62      private static class PlaceholderBlock implements Block, Renderable, RenderCommand
63      {
64          public void render(MarkupWriter writer)
65          {
66          }
67  
68          public void render(MarkupWriter writer, RenderQueue queue)
69          {
70          }
71  
72          @Override
73          public String toString()
74          {
75              return "<PlaceholderBlock>";
76          }
77      }
78  
79      private static final Block PLACEHOLDER_BLOCK = new PlaceholderBlock();
80  
81      private static final ComponentCallback POST_RENDER_CLEANUP = new LifecycleNotificationComponentCallback()
82      {
83          public void run(Component component)
84          {
85              component.postRenderCleanup();
86          }
87      };
88  
89      // For the moment, every component will have a template, even if it consists of
90      // just a page element to queue up a BeforeRenderBody phase.
91  
92      private static void pushElements(RenderQueue queue, List<RenderCommand> list)
93      {
94          int count = size(list);
95          for (int i = count - 1; i >= 0; i--)
96              queue.push(list.get(i));
97      }
98  
99      private static int size(List<?> list)
100     {
101         return list == null ? 0 : list.size();
102     }
103 
104     private abstract class AbstractPhase implements RenderCommand
105     {
106         private final String name;
107 
108         private final boolean reverse;
109 
110         AbstractPhase(String name)
111         {
112             this(name, false);
113         }
114 
115         AbstractPhase(String name, boolean reverse)
116         {
117             this.name = name;
118             this.reverse = reverse;
119         }
120 
121         @Override
122         public String toString()
123         {
124             return phaseToString(name);
125         }
126 
127         void invoke(MarkupWriter writer, Event event)
128         {
129             try
130             {
131                 if (components == null)
132                 {
133                     invokeComponent(coreComponent, writer, event);
134                     return;
135                 }
136 
137                 // Multiple components (i.e., some mixins).
138 
139                 Iterator<Component> i = reverse ? InternalUtils.reverseIterator(components) : components.iterator();
140 
141                 while (i.hasNext())
142                 {
143                     invokeComponent(i.next(), writer, event);
144 
145                     if (event.isAborted())
146                         break;
147                 }
148             }
149             // This used to be RuntimeException, but with TAP5-1508 changes to RenderPhaseMethodWorker, we now
150             // let ordinary exceptions bubble up as well.
151             catch (Exception ex)
152             {
153                 throw new TapestryException(ex.getMessage(), getLocation(), ex);
154             }
155 
156         }
157 
158         /**
159          * Each concrete class implements this method to branch to the corresponding method
160          * of {@link Component}.
161          */
162         protected abstract void invokeComponent(Component component, MarkupWriter writer, Event event);
163     }
164 
165     private class SetupRenderPhase extends AbstractPhase
166     {
167         public SetupRenderPhase()
168         {
169             super("SetupRender");
170         }
171 
172         protected void invokeComponent(Component component, MarkupWriter writer, Event event)
173         {
174             component.setupRender(writer, event);
175         }
176 
177         public void render(MarkupWriter writer, RenderQueue queue)
178         {
179             RenderPhaseEvent event = createRenderEvent(queue);
180 
181             invoke(writer, event);
182 
183             push(queue, event.getResult(), beginRenderPhase, cleanupRenderPhase);
184 
185             event.enqueueSavedRenderCommands();
186         }
187     }
188 
189     private class BeginRenderPhase extends AbstractPhase
190     {
191         private BeginRenderPhase()
192         {
193             super("BeginRender");
194         }
195 
196         protected void invokeComponent(Component component, MarkupWriter writer, Event event)
197         {
198             if (isRenderTracingEnabled())
199                 writer.comment("BEGIN " + component.getComponentResources().getCompleteId() + " (" + getLocation()
200                         + ")");
201 
202             component.beginRender(writer, event);
203         }
204 
205         public void render(final MarkupWriter writer, final RenderQueue queue)
206         {
207             RenderPhaseEvent event = createRenderEvent(queue);
208 
209             invoke(writer, event);
210 
211             push(queue, afterRenderPhase);
212             push(queue, event.getResult(), beforeRenderTemplatePhase, null);
213 
214             event.enqueueSavedRenderCommands();
215         }
216     }
217 
218     /**
219      * Replaces {@link org.apache.tapestry5.internal.structure.ComponentPageElementImpl.BeginRenderPhase} when there is
220      * a handler for AfterRender but not BeginRender.
221      */
222     private class OptimizedBeginRenderPhase implements RenderCommand
223     {
224         public void render(MarkupWriter writer, RenderQueue queue)
225         {
226             push(queue, afterRenderPhase);
227             push(queue, beforeRenderTemplatePhase);
228         }
229 
230         @Override
231         public String toString()
232         {
233             return phaseToString("OptimizedBeginRenderPhase");
234         }
235     }
236 
237     /**
238      * Reponsible for rendering the component's template. Even a component that doesn't have a
239      * template goes through
240      * this phase, as a synthetic template (used to trigger the rendering of the component's body)
241      * will be supplied.
242      */
243     private class BeforeRenderTemplatePhase extends AbstractPhase
244     {
245         private BeforeRenderTemplatePhase()
246         {
247             super("BeforeRenderTemplate");
248         }
249 
250         protected void invokeComponent(Component component, MarkupWriter writer, Event event)
251         {
252             component.beforeRenderTemplate(writer, event);
253         }
254 
255         public void render(final MarkupWriter writer, final RenderQueue queue)
256         {
257             RenderPhaseEvent event = createRenderEvent(queue);
258 
259             invoke(writer, event);
260 
261             push(queue, afterRenderTemplatePhase);
262 
263             if (event.getResult())
264                 pushElements(queue, template);
265 
266             event.enqueueSavedRenderCommands();
267         }
268     }
269 
270     /**
271      * Alternative version of BeforeRenderTemplatePhase used when the BeforeRenderTemplate render
272      * phase is not handled.
273      */
274     private class RenderTemplatePhase implements RenderCommand
275     {
276         public void render(MarkupWriter writer, RenderQueue queue)
277         {
278             push(queue, afterRenderTemplatePhase);
279 
280             pushElements(queue, template);
281         }
282 
283         @Override
284         public String toString()
285         {
286             return phaseToString("RenderTemplate");
287         }
288     }
289 
290     private class BeforeRenderBodyPhase extends AbstractPhase
291     {
292         private BeforeRenderBodyPhase()
293         {
294             super("BeforeRenderBody");
295         }
296 
297         protected void invokeComponent(Component component, MarkupWriter writer, Event event)
298         {
299             component.beforeRenderBody(writer, event);
300         }
301 
302         public void render(final MarkupWriter writer, RenderQueue queue)
303         {
304             RenderPhaseEvent event = createRenderEvent(queue);
305 
306             invoke(writer, event);
307 
308             push(queue, afterRenderBodyPhase);
309 
310             if (event.getResult() && bodyBlock != null)
311                 queue.push(bodyBlock);
312 
313             event.enqueueSavedRenderCommands();
314         }
315     }
316 
317     private class AfterRenderBodyPhase extends AbstractPhase
318     {
319 
320         private AfterRenderBodyPhase()
321         {
322             super("AfterRenderBody", true);
323         }
324 
325         protected void invokeComponent(Component component, MarkupWriter writer, Event event)
326         {
327             component.afterRenderBody(writer, event);
328         }
329 
330         public void render(final MarkupWriter writer, RenderQueue queue)
331         {
332             RenderPhaseEvent event = createRenderEvent(queue);
333 
334             invoke(writer, event);
335 
336             push(queue, event.getResult(), null, beforeRenderBodyPhase);
337 
338             event.enqueueSavedRenderCommands();
339         }
340     }
341 
342     private class AfterRenderTemplatePhase extends AbstractPhase
343     {
344         private AfterRenderTemplatePhase()
345         {
346             super("AfterRenderTemplate", true);
347         }
348 
349         protected void invokeComponent(Component component, MarkupWriter writer, Event event)
350         {
351             component.afterRenderTemplate(writer, event);
352         }
353 
354         public void render(final MarkupWriter writer, final RenderQueue queue)
355         {
356             RenderPhaseEvent event = createRenderEvent(queue);
357 
358             invoke(writer, event);
359 
360             push(queue, event.getResult(), null, beforeRenderTemplatePhase);
361 
362             event.enqueueSavedRenderCommands();
363         }
364     }
365 
366     private class AfterRenderPhase extends AbstractPhase
367     {
368         private AfterRenderPhase()
369         {
370             super("AfterRender", true);
371         }
372 
373         protected void invokeComponent(Component component, MarkupWriter writer, Event event)
374         {
375             component.afterRender(writer, event);
376 
377             if (isRenderTracingEnabled())
378                 writer.comment("END " + component.getComponentResources().getCompleteId());
379         }
380 
381         public void render(final MarkupWriter writer, RenderQueue queue)
382         {
383             RenderPhaseEvent event = createRenderEvent(queue);
384 
385             invoke(writer, event);
386 
387             push(queue, event.getResult(), cleanupRenderPhase, beginRenderPhase);
388 
389             event.enqueueSavedRenderCommands();
390         }
391     }
392 
393     private class CleanupRenderPhase extends AbstractPhase
394     {
395         private CleanupRenderPhase()
396         {
397             super("CleanupRender", true);
398         }
399 
400         protected void invokeComponent(Component component, MarkupWriter writer, Event event)
401         {
402             component.cleanupRender(writer, event);
403         }
404 
405         public void render(final MarkupWriter writer, RenderQueue queue)
406         {
407             RenderPhaseEvent event = createRenderEvent(queue);
408 
409             invoke(writer, event);
410 
411             push(queue, event.getResult(), null, setupRenderPhase);
412 
413             event.enqueueSavedRenderCommands();
414         }
415     }
416 
417     private class PostRenderCleanupPhase implements RenderCommand
418     {
419         /**
420          * Used to detect mismatches calls to {@link MarkupWriter#element(String, Object[])} and
421          * {@link org.apache.tapestry5.MarkupWriter#end()}. The expectation is that any element(s)
422          * begun by this component
423          * during rendering will be balanced by end() calls, resulting in the current element
424          * reverting to its initial
425          * value.
426          */
427         private final Element expectedElementAtCompletion;
428 
429         PostRenderCleanupPhase(Element expectedElementAtCompletion)
430         {
431             this.expectedElementAtCompletion = expectedElementAtCompletion;
432         }
433 
434         public void render(MarkupWriter writer, RenderQueue queue)
435         {
436             renderingValue.set(false);
437 
438             Element current = writer.getElement();
439 
440             if (current != expectedElementAtCompletion)
441                 throw new TapestryException(StructureMessages.unbalancedElements(completeId), getLocation(), null);
442 
443             invoke(false, POST_RENDER_CLEANUP);
444 
445             queue.endComponent();
446         }
447 
448         @Override
449         public String toString()
450         {
451             return phaseToString("PostRenderCleanup");
452         }
453     }
454 
455     private NamedSet<Block> blocks;
456 
457     private BlockImpl bodyBlock;
458 
459     private List<ComponentPageElement> children;
460 
461     private final String elementName;
462 
463     private final Logger eventLogger;
464 
465     private final String completeId;
466 
467     // The user-provided class, with runtime code enhancements. In a component with mixins, this
468     // is the component to which the mixins are attached.
469     private final Component coreComponent;
470 
471     /**
472      * Component lifecycle instances for all mixins; the core component is added to this list during
473      * page load. This is only used in the case that a component has mixins (in which case, the core component is
474      * listed last).
475      */
476     private List<Component> components = null;
477 
478     private final ComponentPageElementResources elementResources;
479 
480     private final ComponentPageElement container;
481 
482     private final InternalComponentResources coreResources;
483 
484     private final String id;
485 
486     private Orderer<Component> mixinBeforeOrderer;
487 
488     private Orderer<Component> mixinAfterOrderer;
489 
490     private boolean loaded;
491 
492     /**
493      * Map from mixin id (the simple name of the mixin class) to resources for the mixin. Created
494      * when first mixin is added.
495      */
496     private NamedSet<InternalComponentResources> mixinIdToComponentResources;
497 
498     private final String nestedId;
499 
500     private final Page page;
501 
502     private final PerThreadValue<Boolean> renderingValue;
503 
504     private final boolean exactParameterCountMatch;
505 
506     // We know that, at the very least, there will be an element to force the component to render
507     // its body, so there's no reason to wait to initialize the list.
508 
509     private final List<RenderCommand> template = CollectionFactory.newList();
510 
511     private RenderCommand setupRenderPhase, beginRenderPhase, beforeRenderTemplatePhase, beforeRenderBodyPhase,
512             afterRenderBodyPhase, afterRenderTemplatePhase, afterRenderPhase, cleanupRenderPhase;
513 
514     /**
515      * Constructor for other components embedded within the root component or at deeper levels of
516      * the hierarchy.
517      *
518      * @param page
519      *         ultimately containing this component
520      * @param container
521      *         component immediately containing this component (may be null for a root component)
522      * @param id
523      *         unique (within the container) id for this component (may be null for a root
524      *         component)
525      * @param elementName
526      *         the name of the element which represents this component in the template, or null
527      *         for
528      *         &lt;comp&gt; element or a page component
529      * @param instantiator
530      *         used to create the new component instance and access the component's model
531      * @param location
532      *         location of the element (within a template), used as part of exception reporting
533      * @param elementResources
534      */
535     ComponentPageElementImpl(Page page, ComponentPageElement container, String id, String nestedId, String completeId,
536                              String elementName, Instantiator instantiator, Location location,
537                              ComponentPageElementResources elementResources)
538     {
539         super(location);
540 
541         this.page = page;
542         this.container = container;
543         this.id = id;
544         this.nestedId = nestedId;
545         this.completeId = completeId;
546         this.elementName = elementName;
547         this.elementResources = elementResources;
548 
549         this.exactParameterCountMatch = page.isExactParameterCountMatch();
550 
551         ComponentResources containerResources = container == null ? null : container.getComponentResources();
552 
553         coreResources = new InternalComponentResourcesImpl(this.page, this, containerResources, this.elementResources,
554                 completeId, nestedId, instantiator, false);
555 
556         coreComponent = coreResources.getComponent();
557 
558         eventLogger = elementResources.getEventLogger(coreResources.getLogger());
559 
560         renderingValue = elementResources.createPerThreadValue();
561 
562         page.addPageLoadedCallback(new Runnable()
563         {
564             public void run()
565             {
566                 pageLoaded();
567             }
568         });
569     }
570 
571     /**
572      * Constructor for the root component of a page.
573      */
574     public ComponentPageElementImpl(Page page, Instantiator instantiator,
575                                     ComponentPageElementResources elementResources)
576     {
577         this(page, null, null, null, page.getName(), null, instantiator, null, elementResources);
578     }
579 
580     private void initializeRenderPhases()
581     {
582         setupRenderPhase = new SetupRenderPhase();
583         beginRenderPhase = new BeginRenderPhase();
584         beforeRenderTemplatePhase = new BeforeRenderTemplatePhase();
585         beforeRenderBodyPhase = new BeforeRenderBodyPhase();
586         afterRenderBodyPhase = new AfterRenderBodyPhase();
587         afterRenderTemplatePhase = new AfterRenderTemplatePhase();
588         afterRenderPhase = new AfterRenderPhase();
589         cleanupRenderPhase = new CleanupRenderPhase();
590 
591         // Now the optimization, where we remove, replace and collapse unused phases. We use
592         // the component models to determine which phases have handler methods for the
593         // render phases.
594 
595         Set<Class> handled = coreResources.getComponentModel().getHandledRenderPhases();
596 
597         for (ComponentResources r : NamedSet.getValues(mixinIdToComponentResources))
598         {
599             handled.addAll(r.getComponentModel().getHandledRenderPhases());
600         }
601 
602         if (!handled.contains(CleanupRender.class))
603             cleanupRenderPhase = null;
604 
605         // Now, work back to front.
606 
607         if (!handled.contains(AfterRender.class))
608             afterRenderPhase = cleanupRenderPhase;
609 
610         if (!handled.contains(AfterRenderTemplate.class))
611             afterRenderTemplatePhase = null;
612 
613         if (!handled.contains(AfterRenderBody.class))
614             afterRenderBodyPhase = null;
615 
616         if (!handled.contains(BeforeRenderTemplate.class))
617             beforeRenderTemplatePhase = new RenderTemplatePhase();
618 
619         if (!handled.contains(BeginRender.class))
620         {
621             RenderCommand replacement = afterRenderPhase != null ? new OptimizedBeginRenderPhase()
622                     : beforeRenderTemplatePhase;
623 
624             beginRenderPhase = replacement;
625         }
626 
627         if (!handled.contains(SetupRender.class))
628             setupRenderPhase = beginRenderPhase;
629     }
630 
631     public ComponentPageElement newChild(String id, String nestedId, String completeId, String elementName,
632                                          Instantiator instantiator, Location location)
633     {
634         ComponentPageElementImpl child = new ComponentPageElementImpl(page, this, id, nestedId, completeId,
635                 elementName, instantiator, location, elementResources);
636 
637         addEmbeddedElement(child);
638 
639         return child;
640     }
641 
642     void push(RenderQueue queue, boolean forward, RenderCommand forwardPhase, RenderCommand backwardPhase)
643     {
644         push(queue, forward ? forwardPhase : backwardPhase);
645     }
646 
647     void push(RenderQueue queue, RenderCommand nextPhase)
648     {
649         if (nextPhase != null)
650             queue.push(nextPhase);
651     }
652 
653     void addEmbeddedElement(ComponentPageElement child)
654     {
655         if (children == null)
656             children = CollectionFactory.newList();
657 
658         String childId = child.getId();
659 
660         for (ComponentPageElement existing : children)
661         {
662             if (existing.getId().equalsIgnoreCase(childId))
663                 throw new TapestryException(StructureMessages.duplicateChildComponent(this, childId), child,
664                         new TapestryException(StructureMessages.originalChildComponent(this, childId,
665                                 existing.getLocation()), existing, null));
666         }
667 
668         children.add(child);
669     }
670 
671     public void addMixin(String mixinId, Instantiator instantiator, String... order)
672     {
673         if (mixinIdToComponentResources == null)
674         {
675             mixinIdToComponentResources = NamedSet.create();
676             components = CollectionFactory.newList();
677         }
678 
679         String mixinExtension = "$" + mixinId.toLowerCase();
680 
681         InternalComponentResourcesImpl resources = new InternalComponentResourcesImpl(page, this, coreResources,
682                 elementResources, completeId + mixinExtension, nestedId + mixinExtension, instantiator, true);
683 
684         mixinIdToComponentResources.put(mixinId, resources);
685         // note that since we're using explicit ordering now,
686         // we don't add anything to components until we page load; instead, we add
687         // to the orderers.
688         if (order == null)
689             order = InternalConstants.EMPTY_STRING_ARRAY;
690 
691         if (resources.getComponentModel().isMixinAfter())
692         {
693             if (mixinAfterOrderer == null)
694                 mixinAfterOrderer = new Orderer<Component>(getLogger());
695             mixinAfterOrderer.add(mixinId, resources.getComponent(), order);
696         } else
697         {
698             if (mixinBeforeOrderer == null)
699                 mixinBeforeOrderer = new Orderer<Component>(getLogger());
700             mixinBeforeOrderer.add(mixinId, resources.getComponent(), order);
701         }
702     }
703 
704     public void bindMixinParameter(String mixinId, String parameterName, Binding binding)
705     {
706         InternalComponentResources mixinResources = NamedSet.get(mixinIdToComponentResources, mixinId);
707 
708         mixinResources.bindParameter(parameterName, binding);
709     }
710 
711     public Binding getBinding(String parameterName)
712     {
713         return coreResources.getBinding(parameterName);
714     }
715 
716     public void bindParameter(String parameterName, Binding binding)
717     {
718         coreResources.bindParameter(parameterName, binding);
719     }
720 
721     public void addToBody(RenderCommand element)
722     {
723         if (bodyBlock == null)
724             bodyBlock = new BlockImpl(getLocation(), "Body of " + getCompleteId());
725 
726         bodyBlock.addToBody(element);
727     }
728 
729     public void addToTemplate(RenderCommand element)
730     {
731         template.add(element);
732     }
733 
734     private void addUnboundParameterNames(String prefix, List<String> unbound, InternalComponentResources resource)
735     {
736         ComponentModel model = resource.getComponentModel();
737 
738         for (String name : model.getParameterNames())
739         {
740             if (resource.isBound(name))
741                 continue;
742 
743             ParameterModel parameterModel = model.getParameterModel(name);
744 
745             if (parameterModel.isRequired())
746             {
747                 String fullName = prefix == null ? name : prefix + "." + name;
748 
749                 unbound.add(fullName);
750             }
751         }
752     }
753 
754     private void pageLoaded()
755     {
756         // If this component has mixins, order them according to:
757         // mixins.
758 
759         if (components != null)
760         {
761             List<Component> ordered = CollectionFactory.newList();
762 
763             if (mixinBeforeOrderer != null)
764                 ordered.addAll(mixinBeforeOrderer.getOrdered());
765 
766             ordered.add(coreComponent);
767 
768             // Add the remaining, late executing mixins
769             if (mixinAfterOrderer != null)
770                 ordered.addAll(mixinAfterOrderer.getOrdered());
771 
772             components = ordered;
773             // no need to keep the orderers around.
774             mixinBeforeOrderer = null;
775             mixinAfterOrderer = null;
776         }
777 
778         initializeRenderPhases();
779 
780         page.addVerifyCallback(new Runnable()
781         {
782             public void run()
783             {
784                 // For some parameters, bindings (from defaults) are provided inside the callback method, so
785                 // that is invoked first, before we check for unbound parameters.
786 
787                 verifyRequiredParametersAreBound();
788             }
789         });
790 
791 
792         loaded = true;
793     }
794 
795     public void enqueueBeforeRenderBody(RenderQueue queue)
796     {
797         if (bodyBlock != null)
798             push(queue, beforeRenderBodyPhase);
799     }
800 
801     public String getCompleteId()
802     {
803         return completeId;
804     }
805 
806     public Component getComponent()
807     {
808         return coreComponent;
809     }
810 
811     public InternalComponentResources getComponentResources()
812     {
813         return coreResources;
814     }
815 
816     public ComponentPageElement getContainerElement()
817     {
818         return container;
819     }
820 
821     public Page getContainingPage()
822     {
823         return page;
824     }
825 
826     public ComponentPageElement getEmbeddedElement(String embeddedId)
827     {
828         ComponentPageElement embeddedElement = null;
829 
830         if (children != null)
831         {
832             for (ComponentPageElement child : children)
833             {
834                 if (child.getId().equalsIgnoreCase(embeddedId))
835                 {
836                     embeddedElement = child;
837                     break;
838                 }
839             }
840         }
841 
842         if (embeddedElement == null)
843         {
844             Set<String> ids = CollectionFactory.newSet();
845 
846             if (children != null)
847             {
848                 for (ComponentPageElement child : children)
849                 {
850                     ids.add(child.getId());
851                 }
852             }
853 
854             throw new UnknownValueException(String.format("Component %s does not contain embedded component '%s'.",
855                     getCompleteId(), embeddedId), new AvailableValues("Embedded components", ids));
856         }
857 
858         return embeddedElement;
859     }
860 
861     public String getId()
862     {
863         return id;
864     }
865 
866     public Logger getLogger()
867     {
868         return coreResources.getLogger();
869     }
870 
871     public Component getMixinByClassName(String mixinClassName)
872     {
873         Component result = mixinForClassName(mixinClassName);
874 
875         if (result == null)
876             throw new TapestryException(StructureMessages.unknownMixin(completeId, mixinClassName), getLocation(), null);
877 
878         return result;
879     }
880 
881     private Component mixinForClassName(String mixinClassName)
882     {
883         if (mixinIdToComponentResources == null)
884             return null;
885 
886         for (InternalComponentResources resources : NamedSet.getValues(mixinIdToComponentResources))
887         {
888             if (resources.getComponentModel().getComponentClassName().equals(mixinClassName))
889             {
890                 return resources
891                         .getComponent();
892             }
893         }
894 
895         return null;
896     }
897 
898     public ComponentResources getMixinResources(String mixinId)
899     {
900         ComponentResources result = NamedSet.get(mixinIdToComponentResources, mixinId);
901 
902         if (result == null)
903             throw new IllegalArgumentException(String.format("Unable to locate mixin '%s' for component '%s'.",
904                     mixinId, completeId));
905 
906         return result;
907     }
908 
909     public String getNestedId()
910     {
911         return nestedId;
912     }
913 
914     public boolean dispatchEvent(ComponentEvent event)
915     {
916         if (components == null)
917             return coreComponent.dispatchComponentEvent(event);
918 
919         // Otherwise, iterate over mixins + core component
920 
921         boolean result = false;
922 
923         for (Component component : components)
924         {
925             result |= component.dispatchComponentEvent(event);
926 
927             if (event.isAborted())
928                 break;
929         }
930 
931         return result;
932     }
933 
934     /**
935      * Invokes a callback on the component instances (the core component plus any mixins).
936      *
937      * @param reverse
938      *         if true, the callbacks are in the reverse of the normal order (this is associated
939      *         with AfterXXX
940      *         phases)
941      * @param callback
942      *         the object to receive each component instance
943      */
944     private void invoke(boolean reverse, ComponentCallback callback)
945     {
946         try
947         { // Optimization: In the most general case (just the one component, no mixins)
948             // invoke the callback on the component and be done ... no iterators, no nothing.
949 
950             if (components == null)
951             {
952                 callback.run(coreComponent);
953                 return;
954             }
955 
956             Iterator<Component> i = reverse ? InternalUtils.reverseIterator(components) : components.iterator();
957 
958             while (i.hasNext())
959             {
960                 callback.run(i.next());
961 
962                 if (callback.isEventAborted())
963                     return;
964             }
965         } catch (RuntimeException ex)
966         {
967             throw new TapestryException(ex.getMessage(), getLocation(), ex);
968         }
969     }
970 
971     public boolean isLoaded()
972     {
973         return loaded;
974     }
975 
976     public boolean isRendering()
977     {
978         return renderingValue.get(false);
979     }
980 
981     /**
982      * Generate a toString() for the inner classes that represent render phases.
983      */
984     private String phaseToString(String phaseName)
985     {
986         return String.format("%s[%s]", phaseName, completeId);
987     }
988 
989     /**
990      * Pushes the SetupRender phase state onto the queue.
991      */
992     public final void render(MarkupWriter writer, RenderQueue queue)
993     {
994         // TODO: An error if the render flag is already set (recursive rendering not
995         // allowed or advisable).
996 
997         // TODO: Check for recursive rendering.
998 
999         renderingValue.set(true);
1000 
1001         queue.startComponent(coreResources);
1002 
1003         queue.push(new PostRenderCleanupPhase(writer.getElement()));
1004 
1005         push(queue, setupRenderPhase);
1006     }
1007 
1008     @Override
1009     public String toString()
1010     {
1011         return String.format("ComponentPageElement[%s]", completeId);
1012     }
1013 
1014     public boolean triggerEvent(String eventType, Object[] contextValues, ComponentEventCallback callback)
1015     {
1016         return triggerContextEvent(eventType, createParameterContext(contextValues == null ? new Object[0]
1017                 : contextValues), callback);
1018     }
1019 
1020     private EventContext createParameterContext(final Object... values)
1021     {
1022         return new AbstractEventContext()
1023         {
1024             public int getCount()
1025             {
1026                 return values.length;
1027             }
1028 
1029             public <T> T get(Class<T> desiredType, int index)
1030             {
1031                 return elementResources.coerce(values[index], desiredType);
1032             }
1033         };
1034     }
1035 
1036     public boolean triggerContextEvent(final String eventType, final EventContext context,
1037                                        final ComponentEventCallback callback)
1038     {
1039         assert InternalUtils.isNonBlank(eventType);
1040         assert context != null;
1041         String description = "Triggering event '" + eventType + "' on " + completeId;
1042 
1043         return elementResources.invoke(description, new Invokable<Boolean>()
1044         {
1045             public Boolean invoke()
1046             {
1047                 return processEventTriggering(eventType, context, callback);
1048             }
1049         });
1050     }
1051 
1052     @SuppressWarnings("all")
1053     private boolean processEventTriggering(String eventType, EventContext context, ComponentEventCallback callback)
1054     {
1055         boolean result = false;
1056 
1057         ComponentPageElement component = this;
1058         String componentId = "";
1059 
1060         // Provide a default handler for when the provided handler is null.
1061         final ComponentEventCallback providedHandler = callback == null ? new NotificationEventCallback(eventType,
1062                 completeId) : callback;
1063 
1064         ComponentEventCallback wrapped = new ComponentEventCallback()
1065         {
1066             public boolean handleResult(Object result)
1067             {
1068                 // Boolean value is not passed to the handler; it will be true (abort event)
1069                 // or false (continue looking for event handlers).
1070 
1071                 if (result instanceof Boolean)
1072                     return (Boolean) result;
1073 
1074                 return providedHandler.handleResult(result);
1075             }
1076         };
1077 
1078         RuntimeException rootException = null;
1079 
1080         // Because I don't like to reassign parameters.
1081 
1082         String currentEventType = eventType;
1083         EventContext currentContext = context;
1084 
1085         // Track the location of the original component for the event, even as we work our way up
1086         // the hierarchy. This may not be ideal if we trigger an "exception" event ... or maybe
1087         // it's right (it's the location of the originally thrown exception).
1088 
1089         Location location = component.getComponentResources().getLocation();
1090 
1091         while (component != null)
1092         {
1093             try
1094             {
1095                 Logger logger = component.getEventLogger();
1096 
1097                 ComponentEvent event = new ComponentEventImpl(currentEventType, componentId, currentContext, wrapped,
1098                         elementResources, exactParameterCountMatch, coreResources.getComponentModel(), logger);
1099 
1100                 logger.debug(TapestryMarkers.EVENT_DISPATCH, "Dispatch event: {}", event);
1101 
1102                 result |= component.dispatchEvent(event);
1103 
1104                 if (event.isAborted())
1105                     return result;
1106             }
1107 
1108             // As with render phase methods, dispatchEvent() can now simply throw arbitrary exceptions
1109             // (the distinction between RuntimeException and checked Exception is entirely in the compiler,
1110             // not the JVM).
1111             catch (Exception ex)
1112             {
1113                 // An exception in an event handler method
1114                 // while we're trying to handle a previous exception!
1115 
1116                 if (rootException != null)
1117                     throw rootException;
1118 
1119                 // We know component is not null and therefore has a component resources that
1120                 // should have a location.
1121 
1122                 // Wrap it up to help ensure that a location is available to the event handler
1123                 // method or,
1124                 // more likely, to the exception report page.
1125 
1126                 rootException = new ComponentEventException(ex.getMessage(), eventType, context, location, ex);
1127 
1128                 // Switch over to triggering an "exception" event, starting in the component that
1129                 // threw the exception.
1130 
1131                 currentEventType = "exception";
1132                 currentContext = createParameterContext(rootException);
1133 
1134                 continue;
1135             }
1136 
1137             // On each bubble up, make the event appear to come from the previous component
1138             // in which the event was triggered.
1139 
1140             componentId = component.getId();
1141 
1142             component = component.getContainerElement();
1143         }
1144 
1145         // If there was a handler for the exception event, it is required to return a non-null (and
1146         // non-boolean) value
1147         // to tell Tapestry what to do. Since that didn't happen, we have no choice but to rethrow
1148         // the (wrapped)
1149         // exception.
1150 
1151         if (rootException != null)
1152             throw rootException;
1153 
1154         return result;
1155     }
1156 
1157     private void verifyRequiredParametersAreBound()
1158     {
1159         List<String> unbound = CollectionFactory.newList();
1160 
1161         addUnboundParameterNames(null, unbound, coreResources);
1162 
1163         List<String> sortedNames = CollectionFactory.newList(NamedSet.getNames(mixinIdToComponentResources));
1164 
1165         Collections.sort(sortedNames);
1166 
1167         for (String name : sortedNames)
1168         {
1169             addUnboundParameterNames(name, unbound, mixinIdToComponentResources.get(name));
1170         }
1171 
1172         if (!unbound.isEmpty())
1173         {
1174             throw new TapestryException(StructureMessages.missingParameters(unbound, this), this, null);
1175         }
1176     }
1177 
1178     public Locale getLocale()
1179     {
1180         return page.getSelector().locale;
1181     }
1182 
1183     public String getElementName(String defaultElementName)
1184     {
1185         return elementName != null ? elementName : defaultElementName;
1186     }
1187 
1188     public Block getBlock(String id)
1189     {
1190         Block result = findBlock(id);
1191 
1192         if (result == null)
1193             throw new BlockNotFoundException(StructureMessages.blockNotFound(completeId, id), getLocation());
1194 
1195         return result;
1196     }
1197 
1198     public Block findBlock(String id)
1199     {
1200         assert InternalUtils.isNonBlank(id);
1201 
1202         return NamedSet.get(blocks, id);
1203     }
1204 
1205     public void addBlock(String blockId, Block block)
1206     {
1207         if (blocks == null)
1208             blocks = NamedSet.create();
1209 
1210         if (!blocks.putIfNew(blockId, block))
1211             throw new TapestryException(StructureMessages.duplicateBlock(this, blockId), block, null);
1212     }
1213 
1214     public String getPageName()
1215     {
1216         return page.getName();
1217     }
1218 
1219     public boolean hasBody()
1220     {
1221         return bodyBlock != null;
1222     }
1223 
1224     public Block getBody()
1225     {
1226         return bodyBlock == null ? PLACEHOLDER_BLOCK : bodyBlock;
1227     }
1228 
1229     public Map<String, Binding> getInformalParameterBindings()
1230     {
1231         return coreResources.getInformalParameterBindings();
1232     }
1233 
1234     public Logger getEventLogger()
1235     {
1236         return eventLogger;
1237     }
1238 
1239     public Link createEventLink(String eventType, Object... context)
1240     {
1241         return elementResources.createComponentEventLink(coreResources, eventType, false, context);
1242     }
1243 
1244     public Link createFormEventLink(String eventType, Object... context)
1245     {
1246         return elementResources.createComponentEventLink(coreResources, eventType, true, context);
1247     }
1248 
1249     protected RenderPhaseEvent createRenderEvent(RenderQueue queue)
1250     {
1251         return new RenderPhaseEvent(new RenderPhaseEventHandler(queue), eventLogger, elementResources);
1252     }
1253 
1254     boolean isRenderTracingEnabled()
1255     {
1256         return elementResources.isRenderTracingEnabled();
1257     }
1258 
1259     public ComponentResourceSelector getResourceSelector()
1260     {
1261         return page.getSelector();
1262     }
1263 }